/*
 * jfreesteel: Serbian eID Viewer Library (GNU LGPLv3)
 * Copyright (C) 2011 Goran Rakic
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License version
 * 3.0 as published by the Free Software Foundation.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, see
 * http://www.gnu.org/licenses/.
 */

package Model;

import com.google.common.primitives.Bytes;
import java.awt.Image;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;


import javax.imageio.ImageIO;
import javax.smartcardio.Card;
import javax.smartcardio.CardChannel;
import javax.smartcardio.CardException;
import javax.smartcardio.CommandAPDU;
import javax.smartcardio.ResponseAPDU;
import javax.swing.ImageIcon;
import javax.swing.JOptionPane;





/**
 * DPICard is a wrapper providing an interface for reading data
 * from the Serbian eID card. Public read*() methods allow you to
 * get specific data about card holder and certificates stored on
 * the card.
 * 
 * It is not advised to initialize this class directly. Instead you
 * should initialize Reader class and assign the listener for the
 * card insertion/removal events. The listener will receive DPICard
 * object when the card is inserted into the terminal.
 * 
 * @author Goran Rakic <grakic@devbase.net>
 * edit by Giovanni Rojas geovaroma@gmail.com
 */
public class DPICard {

    
    
    private Card card = null;
    private CardChannel channel;
    private boolean isDPI;
    private Image photo;
    
    
    
    public DPICard(final Card card) throws IllegalArgumentException, SecurityException, IllegalStateException, CardException
    {
        // Check if the card ATR is recognized
        if(!knownATR(card.getATR().getBytes())) {
            JOptionPane.showMessageDialog(null, "Ingrese un DPI y vuelva a intentarlo.","No es un DPI",0);
            this.isDPI = false;
            
         
        }
        else{
        // Si es un dpi 
            this.isDPI = true;
            
            
        }
        
        // Siempre hare esto, pero no siempre leere la tarjeta dentro ahi. 
        this.card = card;
        channel = card.getBasicChannel();
        
        
    }
    
    private boolean knownATR(byte[] card_atr)
    {
        for(byte[] eid_atr:know_dpi_tars)
        {
            if(Arrays.equals(card_atr, eid_atr)) return true;
        }
        return false;
    }
    
    /* 
     * Esto es para todos los DPI, deben de tener este formato de ATR, que es la manera de respueseta. 
     */
    private static final byte[][] know_dpi_tars = {
            {(byte) 0x3B, (byte) 0xDB, (byte) 0x96, (byte) 0x00, (byte) 0x80, (byte) 0xB1, (byte) 0xFE, (byte) 0x45, (byte) 0x1F, (byte) 0x83, (byte) 0x00, (byte) 0x31, (byte) 0xC0, (byte) 0x64, (byte) 0xC3, (byte) 0x08, (byte) 0x01, (byte) 0x00, (byte)0x0F, (byte)0x90, (byte)0x00, (byte)0x9B }
    };
    
    private static final byte[] DOCUMENT_FILE  = {0x0F, 0x02}; // Document data
    private static final byte[] PERSONAL_FILE  = {0x0F, 0x03}; // Personal data
    private static final byte[] RESIDENCE_FILE = {0x0F, 0x04}; // Place of residence, variable length
    private static final byte[] PHOTO_FILE     = {0x0F, 0x06}; // Personal photo in JPEG format

    @SuppressWarnings("unused")
    private static final byte[] INTERM_CERT_FILE  = {0x0F, 0x11}; // Intermediate CA gradjani public X.509 certificate
    @SuppressWarnings("unused")
    private static final byte[] SIGNING_CERT_FILE = {0x0F, 0x10}; // Public X.509 certificate for qualified (Non Repudiation) signing
    @SuppressWarnings("unused")
    private static final byte[] AUTH_CERT_FILE    = {0x0F, 0x08}; // Public X.509 certificate for authentication

    private static final int BLOCK_SIZE = 0xFF;

    private Map<Integer, byte[]> parseTLV(byte[] bytes)
    {
        HashMap<Integer, byte[]> out = new HashMap<Integer, byte[]>();
        
        // [fld 16bit LE] [len 16bit LE] [len bytes of data] | [fld] [06] ...
        
        int i = 0;
        while(i+3 < bytes.length)
        {
            int len = ((0xFF&bytes[i+3])<<8) + (0xFF&bytes[i+2]);
            int tag = ((0xFF&bytes[i+1])<<8) + (0xFF&bytes[i+0]);
            
            // is there a new tag?
            if(len >= bytes.length) break;

            out.put(new Integer(tag), Arrays.copyOfRange(bytes, i+4, i+4+len));

            i += 4+len;
        }
        
        return out;
    }

    private byte[] readElementaryFile(byte[] name, boolean strip_heading_tlv) throws CardException
    {
        selectFile(name);
    
        // Read first 6 bytes from the EF
        byte[] header = readBinary(0, 6);

        // Missing files have header filled with 0xFF
        int i = 0;
        while(i < header.length && header[i] == 0xFF) i++;
        if(i == header.length) {
            throw new CardException("Read EF file failed: File header is missing");
        }
        
        // Total EF length: data as 16bit LE at 4B offset
        final int length = ((0xFF&header[5])<<8) + (0xFF&header[4]);   
        final int offset = strip_heading_tlv ? 10 : 6;

        // Read binary into buffer
        return readBinary(offset, length);
    }
    
    private byte[] readBinary(int offset, int length) throws CardException
    {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        while(length > 0)
        {
            int block = Math.min(length, BLOCK_SIZE);
            ResponseAPDU r = channel.transmit(new CommandAPDU(0x00, 0xB0, offset >> 8, offset & 0xFF, block));
            if(r.getSW() != 0x9000) {
                throw new CardException("Read binary failed: " + Utils.int2HexString(r.getSW()));
            }

            try {
                byte[] data = r.getData();
                int data_len = data.length;

                out.write(data);
                offset += data_len;
                length -= data_len;
            } catch (IOException e) {
                throw new CardException("Read binary failed: Could not write byte stream");
            }
        }

        return out.toByteArray();
    }
    
    private void selectFile(byte[] name) throws CardException
    {
        ResponseAPDU r = channel.transmit(new CommandAPDU(0x00, 0xA4, 0x08, 0x00, name));
        if(r.getSW() != 0x9000) {
            throw new CardException("Select failed: " + Utils.int2HexString(r.getSW()));
        }
    }
    
    public Image readDPIPhoto() 
    {
        try {
            // Variables que utilizo para leer el DPI
            byte[] ApduArray = null;
            ResponseAPDU response;
            String[] dataDpi= null;
            String dataPrueba = null;
                 
                 
            
            ArrayList<Byte> fotoBytes = new ArrayList<Byte>();
            
            // Onceava Linea   

                ApduArray = new byte []{
                     (byte) 0x00,
                     (byte) 0xB0,
                     (byte) 0x82,
                     (byte) 0x00,
                     (byte) 0x00
                        
                     
                     
                
                     
                     
                };
                response = channel.transmit(new CommandAPDU(ApduArray));

                /* En formato de JPG la imagen debe de iniciar con el byte 0xFF luego de 0xD8 */
                
                if (response.getSW1()==144){
                    System.out.println(response.getData().length);
                    dataPrueba = "";
                    for (int i = 104; i<response.getData().length; i++){
                        dataPrueba+=(char)(response.getData()[i]);
                        fotoBytes.add(response.getData()[i]);

                    }
               
                    System.out.println("Linea 11 : "+dataPrueba);
                        
     // Aqui corresponde los bits de la foto. 
                 for (int b=1; b<90; b++){
                  
                    
                    ApduArray = new byte []{

                        (byte) 0x00,
                        (byte) 0xB0,
                        (byte) b,
                        (byte) 0x00,
                        (byte) 0x00
                            
                    };
                    response = channel.transmit(new CommandAPDU(ApduArray));

                    if (response.getSW1()==144){
                        dataPrueba = "";
                        for (int i = 0; i<response.getData().length; i++){
                            dataPrueba+=(char)(response.getData()[i]);
                            fotoBytes.add(response.getData()[i]);

                        }


                        System.out.println("Linea "+(b+11)+" : "+dataPrueba);
              

                    }            
                 }
                  
                        
                    
                } // Fin de la 11 linea 
            
             byte[] imagen = Bytes.toArray(fotoBytes);
             return new ImageIcon(imagen).getImage();  // retorno la imagen. 
            
        } catch (CardException ex) {
            return null;
        }
    }
    
    public DPIinfo readDPIinfo() throws CardException
    {
        // Variables que utilizo para leer el DPI
        byte[] ApduArray = null;
        ResponseAPDU response;
        String[] dataDpi= null;
        String dataPrueba = null;
        
            
        try {
            
            card.beginExclusive();
            channel = card.getBasicChannel();
            
// Primera linea      
            ApduArray = new byte []{
                 
                 (byte) 160,
                 (byte) 0,
                 (byte) 0,
                 (byte) 0,
                 (byte) 119,
                 (byte) 1,
                 (byte) 131,
                 (byte) 131,
                 (byte) 8,
                 (byte) 16,
                 (byte) 0,
                 (byte) 241,
                 (byte) 0,
                 (byte) 0,
                 (byte) 0,
                 (byte) 1
            };
            // Solo de referencia para mantener el formato de las tarjetas dpi, 
            // lo que se esta mandando es cl_cla, cl_ins_get_uid, p1, p2, datos de referencia, tamanio de vuelta
            response = channel.transmit(new CommandAPDU(0,164,4,12,ApduArray,0));
            
            if (response.getSW1()==144){  // Quiere decir que la respuesta es correcta 144 es 90 en hexadecimal
                // Continuo con la segunda linea
                
                
// Segunda linea      
            ApduArray = new byte []{
                 (byte) 63,
                 (byte) 0
                 
            };
            // Solo de referencia para mantener el formato de las tarjetas dpi, 
            // lo que se esta mandando es cl_cla, cl_ins_get_uid, p1, p2, datos de referencia, tamanio de vuelta
            response = channel.transmit(new CommandAPDU(0,164,0,0,ApduArray,0));
            
            if (response.getSW1()==144){
                // Continuo con la tercera linea
// Terecera linea   
                 
                
            ApduArray = new byte []{
                 (byte) 16,
                 (byte) 1
                 
            };
            // Solo de referencia para mantener el formato de las tarjetas dpi, 
            // lo que se esta mandando es cl_cla, cl_ins_get_uid, p1, p2, datos de referencia, tamanio de vuelta
            response = channel.transmit(new CommandAPDU(0,164,0,0,ApduArray,0));
            
            if (response.getSW1()==144){
                // Continuo con la cuarta linea
                
// Cuarta linea   
                 
                
            ApduArray = new byte []{
                 (byte) 1,
                 (byte) 1
                 
            };
            // Solo de referencia para mantener el formato de las tarjetas dpi, 
            // lo que se esta mandando es cl_cla, cl_ins_get_uid, p1, p2, datos de referencia, tamanio de vuelta
            response = channel.transmit(new CommandAPDU(0,164,2,0,ApduArray,0));
            
            if (response.getSW1()==144){
                // Continuo con la quinta linea 
                
// Quinta linea   
                 
                
            
            // Solo de referencia para mantener el formato de las tarjetas dpi, 
            // lo que se esta mandando es cl_cla, cl_ins_get_uid, p1, p2
             ApduArray = new byte []{
                 (byte) 0,
                 (byte) 176,
                 (byte) 0,
                 (byte) 0,
                 (byte) 0
                 
            };
            response   = channel.transmit(new CommandAPDU(ApduArray));
            
            if (response.getSW1()==144){
                
                // En este momento es donde se empiezan a ver datos por lo que los convertire. 
                String data = "";
                StringBuilder stringBuilder = null;
                for (int i = 0; i<response.getData().length; i++){
                    data+=(char)(response.getData()[i]);
                    
                }
                
                // Creo mi variable de lista 
                ArrayList<String> dataList = new ArrayList<String>(); 
                // Divido Los datos, lo hago de esta manera por orden. 
                
                String dpi = data.substring(0, 13);
                dataList.add(dpi);
                String primerNombre = data.substring(13,38);
                dataList.add(primerNombre);
                String segundoNombre = data.substring(38,46);
                dataList.add(segundoNombre);
                String tercerNombre = data.substring(46,63);
                dataList.add(tercerNombre);
                String primerApellido = data.substring(63,88);
                dataList.add(primerApellido);
                String segundoApellido = data.substring(88,138);
                dataList.add(segundoApellido);
                String sexo = data.substring(138,147);
                dataList.add(sexo);
                String vecindadMunicipio = data.substring(147,177);
                dataList.add(vecindadMunicipio);
                String vecindadDepartamento = data.substring(177,207);
                dataList.add(vecindadDepartamento);
                String nacionalidad = data.substring(207,237);
                dataList.add(nacionalidad);
                String nacimiento = data.substring(237,247);
                dataList.add(nacimiento);
                String emisionFecha = data.substring(247,256);
                dataList.add(emisionFecha);
                
                //Transformo de lista a array de String 
                Object[] objectList = dataList.toArray();
                dataDpi = Arrays.copyOf(objectList,objectList.length,String[].class);
            
                
                
                
                // Continuo con la sexta linea 
              
// Sexta linea   
                 
                
            ApduArray = new byte []{
                 (byte) 0,
                 (byte) 176,
                 (byte) 0,
                 (byte) 255,
                 (byte) 0
                 
            };
            // Solo de referencia para mantener el formato de las tarjetas dpi, 
            // lo que se esta mandando es cl_cla, cl_ins_get_uid, p1, p2, datos de referencia, tamanio de vuelta
            response = channel.transmit(new CommandAPDU(ApduArray));
            
            if (response.getSW1()==144){
                
           
 // Septima Linea               
            ApduArray = new byte []{
                 (byte) 1,
                 (byte) 2
                 
            };
            response = channel.transmit(new CommandAPDU(0,164,2,0,ApduArray,0));
            
            if (response.getSW1()==144){
                dataPrueba = "";
                for (int i = 0; i<response.getData().length; i++){
                    dataPrueba+=(char)(response.getData()[i]);
                    
                }
                System.out.println("Linea 7 : "+dataPrueba);
            
// Octava Linea               
            ApduArray = new byte []{
                 (byte) 0,
                 (byte) 176,
                 (byte) 0,
                 (byte) 0,
                 (byte) 0
                 
            };
            response = channel.transmit(new CommandAPDU(ApduArray));
            
            if (response.getSW1()==144){
                dataPrueba = "";
                for (int i = 0; i<response.getData().length; i++){
                    dataPrueba+=(char)(response.getData()[i]);
                    
                }
                System.out.println("Linea 8 : "+dataPrueba);
            

 // Novena Linea               
            ApduArray = new byte []{
                 (byte) 0,
                 (byte) 0xB0,
                 (byte) 0x00,
                 (byte) 0XFF,
                 (byte) 0
                 
            };
            response = channel.transmit(new CommandAPDU(ApduArray));
            
            if (response.getSW1()==144){
                dataPrueba = "";
                for (int i = 0; i<response.getData().length; i++){
                    dataPrueba+=(char)(response.getData()[i]);
                    
                }
                System.out.println("Linea 9 : "+dataPrueba);
// Decima Linea   

            ApduArray = new byte []{
                 (byte) 0x00,
                 (byte) 0xA4,
                 (byte) 0x04,
                 (byte) 0x0C,
                 (byte) 0x07,
                 (byte) 0xA0,
                 (byte) 0x00,
                 (byte) 0x00,
                 (byte) 0x02,
                 (byte) 0x47,
                 (byte) 0x10,
                 (byte) 0x01,
                 
            };
            response = channel.transmit(new CommandAPDU(ApduArray));
            
            if (response.getSW1()==144){
                dataPrueba = "";
                for (int i = 0; i<response.getData().length; i++){
                    dataPrueba+=(char)(response.getData()[i]);
                    
                }
                System.out.println("Linea 10 : "+dataPrueba);                
                
                
                photo = readDPIPhoto(); // de aqui en adelante es informacion de la foto. 
                
                
            }// Fin de la decima linea
            
            }// Fin de la novena linea
            
            }// Fin de la octava linea
            
            }// Fin de la septima linea
                
            }// Fin de la sexta linea
                
            }// fin de la quinta linea    
                
            }// fin de la cuarta linea               
                
            }// fin de la tercera linea 
                      
            }// fin de la segunda linea 
            
            } // fin de la primera linea
            
                        
            
            
            return new DPIinfo(dataDpi,photo);
            

        } finally {
            card.endExclusive();
                       
        }
    }

    public String debugEidInfo() throws CardException
    {
        try {
            System.out.println("debug exclusive ask");
            card.beginExclusive();
            System.out.println("debug exclusive granted");

            channel = card.getBasicChannel();
            
            final Map<Integer, byte[]> document  = parseTLV(readElementaryFile(DOCUMENT_FILE, false));
            final Map<Integer, byte[]> personal  = parseTLV(readElementaryFile(PERSONAL_FILE, false));
            final Map<Integer, byte[]> residence = parseTLV(readElementaryFile(RESIDENCE_FILE, false));

            String out = "";
            out += "Document:\n"  + Utils.map2UTF8String(document);
            out += "Personal:\n"  + Utils.map2UTF8String(personal);
            out += "Residence:\n" + Utils.map2UTF8String(residence);

            return out;

        } finally {
            card.endExclusive();
            System.out.println("debug exclusive free");
        }        
    }
 
    public void disconnect(boolean reset) throws CardException
    {
        card.disconnect(reset);
        card = null;
    }
    
    @Override
    protected void finalize() throws Throwable
    {
        if(card != null) disconnect(false);
    }

    /*
     * Lo que hace es devolver si la tarjeta ingresada en realidad es un 
     * DPI
     */
    public boolean isOficialDPI() {
        return isDPI;
    }
    /*
     * Lo que hace este metodo es que manda la primera linea para ver 
     * si aun esta insertado el DPI, si me da falso quiere decir que 
     * fue extraida entonces sale del ciclo y continua esperando a que ingrese un DPI. 
     */
    public boolean isCardIn() {
        try {   
            byte [] ApduArray = null;
            
            ApduArray = new byte []{
                 
                 (byte) 160,
                 (byte) 0,
                 (byte) 0,
                 (byte) 0,
                 (byte) 119,
                 (byte) 1,
                 (byte) 131,
                 (byte) 131,
                 (byte) 8,
                 (byte) 16,
                 (byte) 0,
                 (byte) 241,
                 (byte) 0,
                 (byte) 0,
                 (byte) 0,
                 (byte) 1
            };
            
            channel = card.getBasicChannel();
            channel.transmit(new CommandAPDU(0,164,4,12,ApduArray,0));
            
            return true;
        } catch (CardException ex) {
            return false;
        }
        
        
        
    }
    
}
